/*
 * Copyright (C) 2010 Leandro Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.mjl.tracing;

import static java.util.concurrent.TimeUnit.*;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;

import org.aopalliance.intercept.*;

import com.google.code.mjl.*;
import com.google.code.mjl.tracing.impl.*;
import com.google.common.base.*;
import com.google.common.collect.*;

public class TracingInterceptor implements MethodInterceptor {

	private static final int MESSAGE_BUFFER_INITIAL_SIZE = 70;

	private static final ThreadLocal<Boolean> firstInvocation = new ThreadLocal<Boolean>() {
		@Override
		protected Boolean initialValue() {
			return true;
		}
	};

	private static final TraceParser TRACE_PARSER = new TraceParserDefaultImpl();

	/**
	 * Set expiration so we don't end up keeping tracing parameters for cold
	 * methods.
	 */
	private static final ConcurrentMap<Method, TracingParameters> CACHED_TRACING_PARAMETERS = new MapMaker().expiration(60, MINUTES).weakKeys().softValues().makeComputingMap(
			new Function<Method, TracingParameters>() {
				@Override
				public TracingParameters apply(Method from) {
					return TRACE_PARSER.parse(from);
				}
			});

	/**
	 * No expiration because loggers are supposed to exist forever.
	 */
	private static final ConcurrentMap<Class<?>, Log> CACHED_LOGGERS = new MapMaker().makeComputingMap(new Function<Class<?>, Log>() {
		@Override
		public Log apply(Class<?> from) {
			return LogFactory.getLog(from);
		}
	});

	private static final String[] STACKTRACE_CLASSNAMES_TO_OMIT = new String[] { "$EnhancerByGuice", "cglib.proxy.MethodProxy", "InterceptorStackCallback",
			"com.google.code.mjl.tracing.TracingInterceptor" };

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		boolean isFirstInvocation = firstInvocation.get();
		if (isFirstInvocation) {
			firstInvocation.set(false);
		}

		Object target = invocation.getThis();
		Method method = invocation.getMethod();
		Object[] arguments = invocation.getArguments();

		// Target is always a proxy, to get the actual class name, we can just
		// go to its superclass
		Log log = CACHED_LOGGERS.get(target.getClass().getSuperclass());
		TracingParameters parameters = CACHED_TRACING_PARAMETERS.get(method);
		boolean shouldLog = shouldLog(log, parameters, method);

		if (shouldLog) {
			logMessage(log, parameters, target, method, true, arguments);
		}

		try {
			Object result = invocation.proceed();

			if (shouldLog && parameters.getAfter() != null) {
				Object[] resultPlusArgs = new Object[arguments.length + 1];
				resultPlusArgs[0] = result;
				System.arraycopy(arguments, 0, resultPlusArgs, 1, arguments.length);
				logMessage(log, parameters, target, method, false, resultPlusArgs);
			}

			return result;
		} catch (Throwable t) {
			cleanStacktrace(t);
			if (log.enabled(Log.Level.ERROR)) {
				if (parameters != null && parameters.shouldLog(t)) {
					// Only the first invocation in the chain should log the
					// exception
					if (isFirstInvocation) {
						log.error(t, getErrorMessage(target, method, arguments));
					}
				}
			}
			throw t;
		} finally {
			if (isFirstInvocation) {
				firstInvocation.remove();
			}
		}
	}

	protected boolean shouldLog(Log log, TracingParameters parameters, Method method) {
		return parameters != null && log.enabled(parameters.getLevel()) && parameters.shouldLog(method);
	}

	protected void logMessage(Log log, TracingParameters parameters, Object target, Method method, boolean before, Object[] arguments) {
		if (before && parameters.getBefore() != null) {
			log.log(parameters.getLevel(), parameters.isAsync(), parameters.getBefore(), arguments);
		} else if (!before && parameters.getAfter() != null) {
			log.log(parameters.getLevel(), parameters.isAsync(), parameters.getAfter(), arguments);
		} else {
			StringBuilder sb = new StringBuilder(MESSAGE_BUFFER_INITIAL_SIZE);
			sb.append("Invoking ");
			appendInvocationInformation(sb, target, method, arguments);
			log.log(parameters.getLevel(), parameters.isAsync(), sb.toString());
		}
	}

	protected String getErrorMessage(Object target, Method method, Object... arguments) {
		StringBuilder sb = new StringBuilder(MESSAGE_BUFFER_INITIAL_SIZE);
		sb.append("Error invoking ");
		appendInvocationInformation(sb, target, method, arguments);

		return sb.toString();
	}

	protected void appendInvocationInformation(StringBuilder sb, Object target, Method method, Object... arguments) {
		sb.append("method [");
		sb.append(method.getName());
		sb.append("] on object [");
		sb.append(target);
		sb.append("] with arguments ");
		sb.append(Arrays.toString(arguments));
	}

	protected void cleanStacktrace(Throwable t) {
		StackTraceElement[] stackTrace = t.getStackTrace();
		List<StackTraceElement> cleanedStackTrace = Lists.newArrayListWithCapacity(stackTrace.length);
		for (StackTraceElement element : stackTrace) {
			String className = element.getClassName();
			boolean canAddClassName = true;
			for (String name : STACKTRACE_CLASSNAMES_TO_OMIT) {
				if (className.contains(name)) {
					canAddClassName = false;
					break;
				}
			}
			if (canAddClassName) {
				cleanedStackTrace.add(element);
			}
		}
		t.setStackTrace(cleanedStackTrace.toArray(new StackTraceElement[0]));
	}
}
